﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using MugenInjection.Bindings.Builders;
using MugenInjection.Core;
using MugenInjection.Delegates;
using MugenInjection.Exceptions;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Interface.Behaviors;
using MugenInjection.Interface.Components;
using MugenInjection.Scope;

namespace MugenInjection.Bindings
{
    /// <summary>
    /// Represents the base class for binding.
    /// </summary>
    public abstract class BindingBase : DisposableObject, IBinding, IEquatable<BindingBase>
    {
        #region Fields

        private readonly Guid _id = Guid.NewGuid();
        private readonly CanResolveBindingDelegate _canResolve;
        private readonly object _locker = new object();
        private readonly DefaultScopeLifecycleContext _scopeLifecycleContext;

        private readonly Type _service;
        private readonly Type _genericTypeDefinition;
        private readonly bool _isOneService;

        private readonly bool _isGenericTypeDefinition;
        private bool _isCycle;
        private bool _tryDisposeObject;

        private readonly List<Action<IBindingContext>> _activatingActions;
        private readonly List<Action<IBinding>> _deactivatedActions;
        private readonly List<BindingActivatedDelegate<object>> _activatedActions;

        private readonly IScopeLifecycle _scopeLifecycle;
        private readonly IList<IInjectionParameter> _parameters;
        private readonly IList<Type> _services;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="BindingBase" /> class.
        /// </summary>
        /// <param name="services">The specified services type.</param>
        /// <param name="scopeLifecycle">The specified <see cref="IScopeLifecycle"/>.</param>
        /// <param name="parameters">The specified parameters.</param>
        /// <param name="canResolve">The specified condition for can resolve.</param>
        protected BindingBase(IList<Type> services, IScopeLifecycle scopeLifecycle,
                              IEnumerable<IInjectionParameter> parameters, CanResolveBindingDelegate canResolve)
        {
            Validate.ArgumentNotNullAndNonZeroLength(services, "services");
            Validate.ArgumentNotNull(scopeLifecycle, "scopeLifecycle");
            Validate.ArgumentNotNull(canResolve, "canResolve");

            _services = new List<Type>(services);
            InjectorUtilsInternal.RemoveDuplicate(ref _services);
            _isOneService = _services.Count == 1;
            if (_isOneService)
            {
                _service = _services[0];
#if NETFX_CORE
                _isGenericTypeDefinition = _service.GetTypeInfo().IsGenericTypeDefinition;
#else
                _isGenericTypeDefinition = _service.IsGenericTypeDefinition;
#endif
                if (_isGenericTypeDefinition)
                    _genericTypeDefinition = _service.GetGenericTypeDefinition();
            }
            else
            {
                foreach (var service in _services)
                {
#if NETFX_CORE
                    if (!service.GetTypeInfo().IsGenericTypeDefinition) continue;
#else
                    if (!service.IsGenericTypeDefinition) continue;
#endif
                    _isGenericTypeDefinition = true;
                    break;
                }
            }

            Settings = new DictionarySettingsImpl();
            Services = new ReadOnlyCollection<Type>(_services);
            _canResolve = canResolve;
            _scopeLifecycle = scopeLifecycle;
            _scopeLifecycleContext = new DefaultScopeLifecycleContext(this);
            _scopeLifecycle.Context = _scopeLifecycleContext;
            _activatingActions = new List<Action<IBindingContext>>();
            _activatedActions = new List<BindingActivatedDelegate<object>>();
            _deactivatedActions = new List<Action<IBinding>>();
            _parameters = parameters == null
                             ? new List<IInjectionParameter>()
                             : new List<IInjectionParameter>(parameters);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the condition for the binding.
        /// </summary>
        internal CanResolveBindingDelegate Condition
        {
            get { return _canResolve; }
        }

        /// <summary>
        /// Gets the flag that indicates that binding will try to dispose object when dispose binding.
        /// </summary>
        internal bool TryDisposeObject
        {
            get { return _tryDisposeObject; }
            set
            {
                _tryDisposeObject = value;
                Settings.Add(SpecialParameterKeys.TryDisposeObjectSetting, value);
            }
        }

        #endregion

        #region Implementation of IBinding

        /// <summary>
        /// Gets the actions that should be called before instances are activated via the binding.
        /// </summary>
        public ICollection<Action<IBindingContext>> ActivatingActions
        {
            get
            {
                lock (_locker)
                    return _activatingActions;
            }
        }

        /// <summary>
        /// Gets the actions that should be called after instances are activated via the binding.
        /// </summary>
        public ICollection<BindingActivatedDelegate<object>> ActivatedActions
        {
            get
            {
                lock (_locker)
                    return _activatedActions;
            }
        }

        /// <summary>
        /// Gets the actions that should be called before instances are deactivated via the binding.
        /// </summary>
        public ICollection<Action<IBinding>> DeactivatedActions
        {
            get
            {
                lock (_locker)
                    return _deactivatedActions;
            }
        }

        /// <summary>
        /// Gets the lifecycle scope for binding.
        /// </summary>
        public IScopeLifecycle ScopeLifecycle
        {
            get
            {
                lock (_locker)
                    return _scopeLifecycle;
            }
        }

        /// <summary>
        /// Gets the priority.
        /// </summary>
        public BindingPriority Priority { get; internal set; }

        /// <summary>
        /// Gets the settings.
        /// </summary>
        public ISettings Settings { get; private set; }

        /// <summary>
        /// Gets the parameters.
        /// </summary>
        public IList<IInjectionParameter> Parameters
        {
            get
            {
                lock (_locker)
                    return _parameters;
            }
        }

        /// <summary>
        /// Gets the service types.
        /// </summary>
        public IList<Type> Services { get; private set; }

        /// <summary>
        /// Determines whether the specified request can be resolved.
        /// </summary>
        /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
        /// <returns><c>True</c> if the specified service has been resolved; otherwise, <c>false</c>.</returns>
        public virtual bool CanResolve(IBindingContext bindingContext)
        {
            return IsAssignableService(bindingContext.Service) && _canResolve(bindingContext);
        }

        /// <summary>
        /// Resolves instance for the specified <see cref="IBindingContext"/>.
        /// </summary>
        /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
        /// <returns>An instance of the service.</returns>
        public object Resolve(IBindingContext bindingContext)
        {
            Monitor.Enter(_locker);
            try
            {
                object service;
                if (BeginDetectCycle(bindingContext, out service))
                    return service;
                bindingContext.Binding = this;
                _scopeLifecycleContext.BindingContext = bindingContext;
                return _scopeLifecycle.GetObjectFromScope();
            }
            finally
            {
                _scopeLifecycleContext.BindingContext = null;
                _isCycle = false;
                Monitor.Exit(_locker);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Begins detect a cycle dependency.
        /// </summary>
        private bool BeginDetectCycle(IBindingContext bindingContext, out object result)
        {
            result = null;
            if (_isCycle)
            {
                IBehaviorManagerComponent behaviorManagerComponent;
                ICycleDependencyBehavior cycleDependencyBehavior;
                if (bindingContext.CallInjector.Components.TryGet(out behaviorManagerComponent)
                    && behaviorManagerComponent.TryGet(out cycleDependencyBehavior)
                    && cycleDependencyBehavior.Resolve(bindingContext, out result))
                    return true;
                throw new CyclicDependencyException(bindingContext);
            }
            _isCycle = true;
            return false;
        }

        /// <summary>
        /// Merges parameters with the current bindings parameters.
        /// </summary>
        /// <param name="bindingContext">The specified <see cref="IBindingContext"/> to update parameters.</param>
        private void MergeParameter(IBindingContext bindingContext)
        {
            if (_parameters.Count == 0)
                return;
            IList<IInjectionParameter> userParameter = bindingContext.Parameters;
            if (userParameter.Count == 0)
            {
                bindingContext.Parameters = new List<IInjectionParameter>(_parameters);
                return;
            }
            var bindingParameters = new List<IInjectionParameter>(_parameters);
            var newParameters = new List<IInjectionParameter>();
            for (int i = 0; i < userParameter.Count; i++)
            {
                IInjectionParameter injectionParameter = userParameter[i];
                bool find = false;
                for (int index = 0; index < bindingParameters.Count; index++)
                {
                    IInjectionParameter bindingParameter = bindingParameters[index];
                    if (!bindingParameter.Equals(injectionParameter)) continue;
                    bindingParameters[index] = injectionParameter;
                    find = true;
                    break;
                }
                if (find) continue;
                newParameters.Add(injectionParameter);
            }
            bindingParameters.AddRange(newParameters);
            bindingContext.Parameters = bindingParameters;
        }

        /// <summary>
        /// Determines whether the specified type is assignable for current binding.
        /// </summary>
        /// <param name="checkType">The specified <see cref="Type"/> to check.</param>
        /// <returns><c>True</c> if the specified type is assignable; otherwise, <c>false</c>.</returns>
        protected virtual bool IsAssignableService(Type checkType)
        {
            if (_isOneService)
            {
#if NETFX_CORE
                var checkTypeInfo = checkType.GetTypeInfo();
                if (_genericTypeDefinition != null && (checkTypeInfo.IsGenericType))
                    return _genericTypeDefinition == checkTypeInfo.GetGenericTypeDefinition();
#else
                if (_genericTypeDefinition != null && (checkType.IsGenericType))
                    return _genericTypeDefinition == checkType.GetGenericTypeDefinition();
#endif
                return _service == checkType;
            }
            for (int index = 0; index < _services.Count; index++)
            {
                var service = _services[index];
                if (service == checkType)
                    return true;
#if NETFX_CORE
                if (!_isGenericTypeDefinition || !service.GetTypeInfo().IsGenericTypeDefinition || !checkType.GetTypeInfo().IsGenericType) continue;
#else
                if (!_isGenericTypeDefinition || !service.IsGenericTypeDefinition || !checkType.IsGenericType) continue;
#endif
                if (service.GetGenericTypeDefinition() == checkType.GetGenericTypeDefinition())
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Resolves instance for the specified <see cref="IBindingContext"/>.
        /// </summary>
        /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
        /// <returns>An instance of the service.</returns>
        internal object ResolveServiceInternal(IBindingContext bindingContext)
        {
            if (_activatingActions.Count != 0)
                OnActivating(bindingContext);
            if (_parameters.Count != 0)
                MergeParameter(bindingContext);
            var service = ResolveService(bindingContext);
            if (_activatedActions.Count != 0)
                OnActivated(bindingContext, ref service);
            return service;
        }

        /// <summary>
        /// Resolves instance for the specified <see cref="IBindingContext"/>.
        /// </summary>
        /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
        /// <returns>An instance of the service.</returns>
        protected abstract object ResolveService(IBindingContext bindingContext);

        #endregion

        #region Events

        /// <summary>
        /// On activating the <see cref="BindingBase"/>.
        /// </summary>
        /// <param name="bindingContext"></param>
        private void OnActivating(IBindingContext bindingContext)
        {
            for (int index = 0; index < _activatingActions.Count; index++)
            {
                Action<IBindingContext> activatingAction = _activatingActions[index];
                if (activatingAction == null) continue;
                activatingAction(bindingContext);
            }
        }

        /// <summary>
        /// On activated the <see cref="BindingBase"/>.
        /// </summary>
        private void OnActivated(IBindingContext bindingContext, ref object obj)
        {
            for (int index = 0; index < _activatedActions.Count; index++)
            {
                var activatedAction = _activatedActions[index];
                if (activatedAction == null) continue;
                activatedAction(this, bindingContext, ref obj);
            }
        }

        /// <summary>
        /// On disposed the <see cref="BindingBase"/>.
        /// </summary>
        private void OnDisposed()
        {
            for (int index = 0; index < _deactivatedActions.Count; index++)
            {
                Action<IBinding> deactivatedAction = _deactivatedActions[index];
                if (deactivatedAction == null) continue;
                deactivatedAction(this);
            }
        }

        #endregion

        #region Overrides of DisposableObject

        /// <summary>
        /// Releases resources held by the object.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing && !IsDisposed)
            {
                OnDisposed();
                _scopeLifecycle.Dispose();
                _activatingActions.Clear();
                _activatedActions.Clear();
                _deactivatedActions.Clear();
            }
            base.Dispose(disposing);
        }

        #endregion

        #region Equality members

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <returns>
        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
        /// </returns>
        /// <param name="other">An object to compare with this object.</param>
        public bool Equals(BindingBase other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;
            return _id.Equals(other._id);
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </returns>
        public override string ToString()
        {
            return InjectorUtilsInternal.FormatBinding(this);
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
        /// </returns>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>. </param><filterpriority>2</filterpriority>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;
            return Equals((BindingBase)obj);
        }

        /// <summary>
        /// Serves as a hash function for a particular type. 
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        public override int GetHashCode()
        {
            return _id.GetHashCode();
        }

        public static bool operator ==(BindingBase left, BindingBase right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(BindingBase left, BindingBase right)
        {
            return !Equals(left, right);
        }

        #endregion
    }
}